package fr.gdi.android.news.utils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Thread.State;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;

/**
 * This is an object that can load images from a URL on a thread.
 * 
 * @author Jeremy Wadsack
 */
public class AsyncImageLoader
{
    private static final String TAG = AsyncImageLoader.class.getName();
    
    // Global cache of images.
    // Using SoftReference to allow garbage collector to clean cache if needed
    private final HashMap<String, SoftReference<Bitmap>> cache;
    
    private final class QueueItem
    {
        public URL url;
        public ImageLoadedListener listener;
    }
    
    private final ArrayList<QueueItem> queue = new ArrayList<QueueItem>();
    
    private final Handler handler = new Handler();
    
    private Thread thread;
    private QueueRunner runner = new QueueRunner();;
    
    /** Creates a new instance of the ImageThreadLoader */
    public AsyncImageLoader(Context context)
    {
        thread = new Thread(runner);
        cache = new ImageCache(context);
    }
    
    /**
     * Defines an interface for a callback that will handle responses from the
     * thread loader when an image is done being loaded.
     */
    public interface ImageLoadedListener
    {
        public void imageLoaded(Bitmap imageBitmap);
    }
    
    /**
     * Provides a Runnable class to handle loading the image from the URL and
     * settings the ImageView on the UI thread.
     */
    private class QueueRunner implements Runnable
    {
        public void run()
        {
            synchronized (this)
            {
                while (queue.size() > 0)
                {
                    final QueueItem item = queue.remove(0);
                    
                    if ( item == null || item.url == null || TextUtils.isEmpty(item.url.toString()) ) continue;
                    
                    // If in the cache, return that copy and be done
                    if (cache.containsKey(item.url.toString()) && cache.get(item.url.toString()) != null)
                    {
                        // Use a handler to get back onto the UI thread for the
                        // update
                        handler.post(new Runnable() {
                            public void run()
                            {
                                if (item.listener != null)
                                {
                                    // NB: There's a potential race condition
                                    // here where the cache item could get
                                    // garbage collected between when we post
                                    // the runnable and it's executed.
                                    // Ideally we would re-run the network load
                                    // or something.
                                    SoftReference<Bitmap> ref = cache.get(item.url.toString());
                                    if (ref != null)
                                    {
                                        item.listener.imageLoaded(ref.get());
                                    }
                                }
                            }
                        });
                    }
                    else
                    {
                        try 
                        {
                            Runnable runnable = new Runnable() {
                                public void run()
                                {
                                    if (item.listener != null)
                                    {
                                        item.listener.imageLoaded(null);
                                    }
                                }
                            };
                            
                            Bitmap bmp = readBitmapFromNetwork(item.url);
                            if (bmp != null)
                            {
                                cache.put(item.url.toString(), new SoftReference<Bitmap>(bmp));
                                bmp = null;
                            }
                            
                            // Use a handler to get back onto the UI thread for
                            // the update
                            handler.post(runnable);
                        }
                        catch ( OutOfMemoryError e )
                        {
                            Log.e(Constants.PACKAGE, "Caught OutOfMemoryError while decoding images.");
                            break;
                        }
                    }
                    
                }
            }
        }
    }
    
    /**
     * Queues up a URI to load an image from for a given image view.
     * 
     * @param uri
     *            The URI source of the image
     * @param callback
     *            The listener class to call when the image is loaded
     * @throws MalformedURLException
     *             If the provided uri cannot be parsed
     * @return A Bitmap image if the image is in the cache, else null.
     */
    public Bitmap loadImage(String uri, final ImageLoadedListener listener) throws MalformedURLException
    {
        //uri = uri.replaceAll("&amp;", "&"); 
        // If it's in the cache, just get it and quit it
        if (cache.containsKey(uri))
        {
            SoftReference<Bitmap> ref = cache.get(uri);
            if (ref != null)
            {
                Bitmap bmp = ref.get();
                listener.imageLoaded(bmp);
                return bmp;
            }
        }
        
        QueueItem item = new QueueItem();
        item.url = new URL(uri);
        item.listener = listener;
        queue.add(item);
        
        // start the thread if needed
        if (thread.getState() == State.NEW)
        {
            thread.start();
        }
        else if (thread.getState() == State.TERMINATED)
        {
            thread = new Thread(runner);
            thread.start();
        }
        return null;
    }
    
    public void loadImage(final Set<String> uris, final ImageLoadedListener listener)
    {
        for (String uri : uris)
        {
            try
            {
                loadImage(uri, listener);
            }
            catch (Exception e)
            {
                // swallow
            }
        }
    }
    
    /**
     * Convenience method to retrieve a bitmap image from a URL over the
     * network. The built-in methods do not seem to work, as they return a
     * FileNotFound exception.
     * 
     * Note that this does not perform any threading -- it blocks the call while
     * retrieving the data.
     * 
     * @param url
     *            The URL to read the bitmap from.
     * @return A Bitmap image or null if an error occurs.
     */
    private static Bitmap readBitmapFromNetwork(URL url)
    {
        InputStream is = null;
        BufferedInputStream bis = null;
        Bitmap bmp = null;
        try
        {
            URLConnection conn = url.openConnection();
            conn.connect();
            is = conn.getInputStream();
            bis = new BufferedInputStream(is);
            bmp = BitmapFactory.decodeStream(bis);
        }
        catch (MalformedURLException e)
        {
            Log.e(TAG, "Bad ad URL", e);
        }
        catch (IOException e)
        {
            Log.e(TAG, "Could not get remote image", e);
        }
        finally
        {
            try
            {
                if (is != null) is.close();
                if (bis != null) bis.close();
            }
            catch (IOException e)
            {
                Log.w(TAG, "Error closing stream.");
            }
        }
        return bmp;
    }
    
    @SuppressWarnings("serial")
    private static class ImageCache extends HashMap<String, SoftReference<Bitmap>>
    {
        private Context context;
        
        public ImageCache(Context context)
        {
            this.context = context;
        }
        
        @Override
        public SoftReference<Bitmap> get(Object key)
        {
            SoftReference<Bitmap> bitmap = IOUtils.getCachedImage(context, (String) key, null);
            
            if (bitmap != null)
            {
                return bitmap;
            }
            return super.get(key);
        }
        
        @Override
        public SoftReference<Bitmap> put(String key, SoftReference<Bitmap> value)
        {
            Bitmap bmp = value.get();
            
            if (bmp != null)
            {
                IOUtils.writeImage(context, bmp, (String) key, null);
                
                return super.put(key, value);
            }
            
            return null;
        }
        
    }
    
}
